package bancosys.tec.persist.persister;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import jmine.tec.persist.PersistMessages;
import jmine.tec.utils.debug.impl.DebugRuntimeFactory;
import jmine.tec.utils.dependency.DependencyGraph;
import jmine.tec.utils.reflection.UnmodifiableChecker;
import jmine.tec.utils.register.Receiver;

import org.hibernate.SessionFactory;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.audit.AuditManager;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.persister.listener.PersisterListener;
import bancosys.tec.persist.persister.listener.PurePersisterListener;
import bancosys.tec.persist.validator.ValidatorFactory;
import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Factory de Persister.
 */
public class PersisterFactory implements Receiver<PersisterListener<?>> {

    private ValidatorFactory validatorFactory;

    private AuditManager auditManager;

    /**
     * Nome do package onde ficam os beans.
     */
    public static final String BEAN_PACKAGE = "domain";

    /**
     * Nome do package onde ficam os daos.
     */
    public static final String PERSISTER_PACKAGE = "persister";

    private final DependencyGraph<PersisterListener<?>> listenersGraph = new DependencyGraph<PersisterListener<?>>();

    private final Map<Class<?>, Set<? extends PersisterListener<?>>> listenersByType =
            new ConcurrentHashMap<Class<?>, Set<? extends PersisterListener<?>>>();

    private final List<PurePersisterListener<?>> pureListeners = new LinkedList<PurePersisterListener<?>>();

    private final Map<Class<?>, Set<? extends PurePersisterListener<?>>> pureListenersByType =
            new ConcurrentHashMap<Class<?>, Set<? extends PurePersisterListener<?>>>();

    private final Map<Class<?>, Class<?>> persisterClassCache = new ConcurrentHashMap<Class<?>, Class<?>>();

    /**
     * Registra o Listener que serão obtidos na criacao do Persister.
     * 
     * @param listenerList Lista de Listeners que serão injetados via spring.
     */
    public void register(Collection<? extends PersisterListener<?>> listenerList) {
        if (DebugRuntimeFactory.ENABLED) {
            for (PersisterListener<?> persisterListener : listenerList) {
                if (!UnmodifiableChecker.isUnmodifiableAnnotationPresent(persisterListener.getClass())) {
                    System.err.println("persisterListener of type " + persisterListener.getClass() + " is not @Unmodifiable!");
                }
            }
        }
        this.listenersGraph.addValues(listenerList);
    }

    /**
     * Define o gerenciador de auditoria.
     * 
     * @param auditManager gerenciador de auditoria.
     */
    public void setAuditManager(AuditManager auditManager) {
        this.auditManager = auditManager;
    }

    /**
     * Define a validator factory.
     * 
     * @param validatorFactory validator factory.
     */
    public void setValidatorFactory(ValidatorFactory validatorFactory) {
        this.validatorFactory = validatorFactory;
    }

    /**
     * Recupera o persister puro.
     * 
     * @param sessionFactory session factory do hibernate.
     * @param bean bean.
     * @param <T> classe do bean.
     * @return devolve o persister.
     */
    public <T extends Persistable> Persister<T> getPurePersister(SessionFactory sessionFactory, T bean) {
        PurePersister<T> persister = new PurePersister<T>();
        persister.setTarget(bean);
        persister.setSessionFactory(sessionFactory);
        persister.setValidatorFactory(this.validatorFactory);
        persister.setAuditManager(this.auditManager);
        @SuppressWarnings("unchecked")
        Class<T> literal = (Class<T>) bean.getClass();
        persister.setPersisterListeners(this.getPurePersisterListeners(literal));
        return persister;
    }

    /**
     * Recupera o Persiter a partir de sua classe. Este método assume o padrão que o Persiter está no pacote .persiter a partir do pacote da
     * classe, e que possui 'Persiter' no fim do nome. Exemplo: Classe: bancosys.tec.persist.domain.base.FirstDummy DAO:
     * bancosys.tec.persist.persiter.base.FirstDummyPersister
     * 
     * @param bean o bean.
     * @param sessionFactory session factory do gibernate.
     * @param <T> classe do bean.
     * @return o persiter
     */
    @SuppressWarnings("unchecked")
    public <T extends Persistable> Persister<T> getPersister(SessionFactory sessionFactory, T bean) {
        final Class<? extends Persistable> boClass = bean.getClass();
        Class<?> persisterClass = this.persisterClassCache.get(boClass);
        if (persisterClass == null) {
            persisterClass = this.getPersisterClass(boClass);
            if (persisterClass == null) {
                persisterClass = DefaultPersister.class;
            }
            this.persisterClassCache.put(boClass, persisterClass);
        }
        Persister<T> persister;
        try {
            persister = (Persister<T>) persisterClass.getConstructor().newInstance();
        } catch (SecurityException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_PERSISTER.create(persisterClass.getName()), e);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_PERSISTER.create(persisterClass.getName()), e);
        } catch (InstantiationException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_PERSISTER.create(persisterClass.getName()), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_PERSISTER.create(persisterClass.getName()), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_PERSISTER.create(persisterClass.getName()),
                    e.getTargetException());
        } catch (NoSuchMethodException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_PERSISTER.create(persisterClass.getName()), e);
        }
        Set<PersisterListener<T>> pListeners = this.getPersisterListeners(bean.getClass());
        persister.setListeners(pListeners);
        persister.setSessionFactory(sessionFactory);
        persister.setValidatorFactory(this.validatorFactory);
        persister.setTarget(bean);
        persister.setAuditManager(this.auditManager);
        return persister;
    }

    /**
     * Dada uma classe devolve a classe do seu persister.
     * 
     * @param boClass classe.
     * @param <T> classe do bean.
     * @return classe do persiter.
     */
    @SuppressWarnings("unchecked")
    protected <T extends Persistable> Class<? super Persister<T>> getPersisterClass(Class<T> boClass) {
        Class<Persister<T>> persisterClass = null;
        int index = boClass.getName().indexOf("." + PersisterFactory.BEAN_PACKAGE + ".");
        if (index == -1) {
            throw new PersistenceException(PersistMessages.BEAN_NOT_ON_STANDARD_PACKAGE.create(boClass.getName()));
        }
        String classNamePrefix = boClass.getName().substring(0, index);
        String classNameSuffix = boClass.getName().substring(index + ("." + PersisterFactory.BEAN_PACKAGE + ".").length());
        String persisterClassName = classNamePrefix + "." + PersisterFactory.PERSISTER_PACKAGE + "." + classNameSuffix + "Persister";
        try {
            persisterClass = (Class<Persister<T>>) ReflectionUtils.findClass(persisterClassName, this.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            Class<? super T> superclass = boClass.getSuperclass();

            // (1) superclass pode ser nulo em alguns casos bizarros
            // (2) superclass deve ser um Persistable
            // (3) superclass deve estar no pacote de beans
            if (superclass != null && Persistable.class.isAssignableFrom(superclass)
                    && superclass.getName().contains("." + PersisterFactory.BEAN_PACKAGE + ".")) {

                // se superclass for Object, então chegamos no fim da recursão
                if (superclass != Object.class) {
                    return (Class<? super Persister<T>>) this.getPersisterClass((Class<? extends Persistable>) superclass);
                }
            }
        }
        return persisterClass;
    }

    /**
     * Devolve a fabrica de validadores.
     * 
     * @return a fabrica de validadores.
     */
    public ValidatorFactory getValidatorFactory() {
        return this.validatorFactory;
    }

    /**
     * Devolve o gerenciador de auditoria.
     * 
     * @return o gerenciador de auditoria.
     */
    public AuditManager getAuditManager() {
        return this.auditManager;
    }

    /**
     * Obtem a lista de persisterListeners que devem ser utilizados para persistir um bean. Devem ser incluidos nessa lista, listeners que
     * foram registrados para: <br>
     * <li>A classe do bean</li> <li>Super-classes do bean</li> <li>Interfaces que o bean implementa</li>
     * 
     * @param persistableClass persistableClass
     * @param <T> classe do bean.
     * @return lista contendo os persisterListeners que devem ser utilizados para persistir um bean
     */
    private <T extends Persistable> Set<PersisterListener<T>> getPersisterListeners(Class<?> persistableClass) {
        @SuppressWarnings("unchecked")
        Set<PersisterListener<T>> set = (Set<PersisterListener<T>>) this.listenersByType.get(persistableClass);
        if (set != null) {
            return set;
        }
        Set<PersisterListener<T>> beanListeners = new LinkedHashSet<PersisterListener<T>>();
        @SuppressWarnings("unchecked")
        List<PersisterListener<T>> topologicalOrder = (List<PersisterListener<T>>) this.listenersGraph.getTopologicalOrder();
        for (PersisterListener<T> persisterListener : topologicalOrder) {
            if (persisterListener.getTargetClass().isAssignableFrom(persistableClass)) {
                beanListeners.add(persisterListener);
            }
        }
        this.listenersByType.put(persistableClass, beanListeners);
        return beanListeners;
    }

    /**
     * Devolve os {@link PurePersisterListener}'s para a classe passada
     * 
     * @param <T> o tipo do {@link PurePersisterListener}
     * @param persistableClass a classe
     * @return {@link Set} de {@link PurePersisterListener} para a classe passda
     */
    private <T extends Persistable> Set<? extends PurePersisterListener<T>> getPurePersisterListeners(Class<T> persistableClass) {
        @SuppressWarnings("unchecked")
        Set<PurePersisterListener<T>> set = (Set<PurePersisterListener<T>>) this.pureListenersByType.get(persistableClass);
        if (set != null) {
            return set;
        }
        set = new LinkedHashSet<PurePersisterListener<T>>();
        for (PurePersisterListener<?> ppl : this.pureListeners) {
            if (ppl.entityType().isAssignableFrom(persistableClass)) {
                @SuppressWarnings("unchecked")
                final PurePersisterListener<T> localPersisterListener = (PurePersisterListener<T>) ppl;
                set.add(localPersisterListener);
            }
        }
        this.pureListenersByType.put(persistableClass, set);
        return set;
    }

    /**
     * @return uma instancia de {@link PurePersisterListenerRegistration}
     */
    public PurePersisterListenerRegistration getRegistrator() {
        return new PurePersisterListenerRegistration(this);
    }

    /**
     * Classe opaca para poder registrar {@link PurePersisterListener}
     * 
     * @author takeshi
     */
    public static class PurePersisterListenerRegistration implements Receiver<PurePersisterListener<Object>> {
        private final PersisterFactory factoryToRegister;

        /**
         * C'tor Recebe o {@link PersisterFactory} que vai receber os {@link PurePersisterListener}
         * 
         * @param factoryToRegister o {@link PersisterFactory}
         */
        public PurePersisterListenerRegistration(PersisterFactory factoryToRegister) {
            super();
            this.factoryToRegister = factoryToRegister;
        }

        /**
         * {@inheritDoc}
         * 
         * @see jmine.tec.utils.register.Receiver#register(java.util.Collection)
         */
        public void register(Collection<? extends PurePersisterListener<Object>> registers) {
            // verificar @Unmodifiable
            if (DebugRuntimeFactory.ENABLED) {
                for (PurePersisterListener<?> persisterListener : registers) {
                    if (!UnmodifiableChecker.isUnmodifiableAnnotationPresent(persisterListener.getClass())) {
                        System.err.println("persisterListener of type " + persisterListener.getClass() + " is not @Unmodifiable!");
                    }
                }
            }
            this.factoryToRegister.pureListeners.addAll(registers);
        }

    }

}
